#import "BWDiamondCanvas.h"
#import "BWQueenStitch.h"
#import "BWQueenStitchSet.h"
#import "BWFrenchKnotSet.h"

#define CELL_SIZE 24.0
#define KNOT_SIZE 15.0

@interface BWDiamondCanvas (ForwardReferences)

- (NSRect) unrotatedArea;

@end // BWDiamondCanvas


static float min4 (float f1, float f2, float f3, float f4)
{
    float x1 = MIN (f1, f2);
    float x2 = MIN (f3, f4);

    float x3 = MIN (x1, x2);

    return (x3);

} // min4


static float max4 (float f1, float f2, float f3, float f4)
{
    float x1 = MAX (f1, f2);
    float x2 = MAX (f3, f4);

    float x3 = MAX (x1, x2);

    return (x3);

} // max4


NSRect rectFromPoints (NSPoint anchor, NSPoint opposite)
{
    float minx, maxx, miny, maxy;
    minx = MIN (anchor.x, opposite.x);
    maxx = MAX (anchor.x, opposite.x);
    miny = MIN (anchor.y, opposite.y);
    maxy = MAX (anchor.y, opposite.y);
    
    NSRect rect;
    rect.origin.x = minx;
    rect.origin.y = miny;
    rect.size.width = maxx - minx;
    rect.size.height = maxy - miny;
    
    return (rect);
    
} // rectFromPoints


NSRect rectFrom4Points (NSPoint p1, NSPoint p2, NSPoint p3, NSPoint p4)
{
    float minx = min4 (p1.x, p2.x, p3.x, p4.x);
    float maxx = max4 (p1.x, p2.x, p3.x, p4.x);
    float miny = min4 (p1.y, p2.y, p3.y, p4.y);
    float maxy = max4 (p1.y, p2.y, p3.y, p4.y);

    NSRect rect;
    rect.origin.x = minx;
    rect.origin.y = miny;
    rect.size.width = maxx - minx;
    rect.size.height = maxy - miny;

    return (rect);

} // rectFrom4Points


@implementation BWDiamondCanvas


- (id) initWithFrame: (NSRect) frame
{
    if (![super initWithFrame: frame]) {
        return (nil);
    }

    [self setOffsetX: 0.0  y: 0.0];
    backgroundColor = [[NSColor whiteColor] retain];

    zoom = 100; // 100% zoom by default

    trackMode = kTrackDiamonds;

    return (self);

} // initWithFrame


- (BWDiamondCanvas *) initForPrintingWithStitches: (BWQueenStitchSet *) s
                                  backgroundColor: (NSColor *) bg
                                       areaAnchor: (BWGrid_Square) anchor
                                     areaOpposite: (BWGrid_Square) opposite
{
    // can't init with frame because we don't have a frame yet
    
    if (![super init]) {
        return (nil);
    }

    [self setStitches: s];
    [self setAreaAnchor: anchor];
    [self setAreaOpposite: opposite];
    [self setBackgroundColor: bg];
    
    // calculate the bounds of the view in non-rotate coordinates
    NSRect bounds = [self unrotatedArea];

    // doing both seem necessary.  Not setting the frame size
    // causes a "no pages from the document were selected to be
    // printed" complaint
    [self setFrameSize: bounds.size];
    [self setFrame: bounds];

    [self setOffsetX: -bounds.origin.x
          y: -bounds.origin.y];

    bounds.origin = NSZeroPoint;
    [self setBounds: bounds];

    return (self);

} // initForPrintingWithStitches



- (void) dealloc
{
    [rotator release];
    [inverseRotator release];
    [stitches release];
    [frenchKnots release];

    [super dealloc];

} // dealloc


- (void) awakeFromNib
{
    // for ease of debugging to turn back on
//    [[self window] setAcceptsMouseMovedEvents: YES];
} // awakeFromNib


- (void) mapPoint: (NSPoint) point  toSquare: (BWGrid_Square *) square
{
    square->x = point.x / CELL_SIZE;
    square->y = point.y / CELL_SIZE;

    if (point.y < 0) {
        square->y--;
    }
    if (point.x < 0) {
        square->x--;
    }

} // mapPoint


- (void) mapPoint: (NSPoint) point  toSquareIntersection: (BWGrid_Square *) square
{
    point.x += CELL_SIZE / 2.0;
    point.y += CELL_SIZE / 2.0;
    
    square->x = point.x / CELL_SIZE;
    square->y = point.y / CELL_SIZE;

    if (point.y < 0) {
        square->y--;
    }
    if (point.x < 0) {
        square->x--;
    }

} // mapPoint (to intersection)


- (NSRect) rectForSquare: (BWGrid_Square) square
{
    NSRect rect;
    rect.origin.x = square.x * CELL_SIZE;
    rect.origin.y = square.y * CELL_SIZE;
    rect.size.width = rect.size.height = CELL_SIZE;

    return (rect);

} // rectForSquare


- (NSRect) rectForIntersection: (BWGrid_Square) square
{
    NSRect rect = [self rectForSquare: square];
    
    // want the top part of the diamond
    NSPoint point = NSMakePoint (NSMinX(rect), NSMinY(rect));
    
    NSRect knotRect;
    knotRect.origin.x = point.x - KNOT_SIZE / 2.0;
    knotRect.origin.y = point.y - KNOT_SIZE / 2.0;
    knotRect.size = NSMakeSize (KNOT_SIZE, KNOT_SIZE);

    return (knotRect);

} // rectForIntersection



- (void) drawGrid
{
    NSRect bounds = [self bounds];

    // take the four corners of the view and transform them
    // into rotated coordinates

    NSPoint topLeft, topRight, bottomLeft, bottomRight;
    topLeft = NSMakePoint (NSMinX(bounds), NSMaxY(bounds));
    topRight = NSMakePoint (NSMaxX(bounds), NSMaxY(bounds));
    bottomLeft = NSMakePoint (NSMinX(bounds), NSMinY(bounds));
    bottomRight = NSMakePoint (NSMaxX(bounds), NSMinY(bounds));

    topLeft = [inverseRotator transformPoint: topLeft];
    topRight = [inverseRotator transformPoint: topRight];
    bottomLeft = [inverseRotator transformPoint: bottomLeft];
    bottomRight = [inverseRotator transformPoint: bottomRight];
    
    // find the extremes

    float minX = min4 (topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
    float maxX = max4 (topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
    float minY = min4 (topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
    float maxY = max4 (topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);

    // need the origin so the grid will be anchored in a uniform place
    BWGrid_Square square;
    NSRect topLeftSquare;

    [self mapPoint: topLeft toSquare: &square];
    topLeftSquare = [self rectForSquare: square];

    float x, y;

    NSPoint from, to;

    // NE-SW lines
    for (x = topLeftSquare.origin.x; x < maxX; x += CELL_SIZE) {
        from = [rotator transformPoint: NSMakePoint(x, minY)];
        to = [rotator transformPoint: NSMakePoint (x, maxY)];
        [NSBezierPath strokeLineFromPoint: from
                      toPoint: to];
    }
    for (x = topLeftSquare.origin.x - CELL_SIZE; x > minX; x -= CELL_SIZE) {
        from = [rotator transformPoint: NSMakePoint(x, minY)];
        to = [rotator transformPoint: NSMakePoint (x, maxY)];
        [NSBezierPath strokeLineFromPoint: from
                      toPoint: to];
    }

    // NW-SE lines
    for (y = topLeftSquare.origin.y;
         y < maxY;
         y += CELL_SIZE) {
        from = [rotator transformPoint: NSMakePoint(minX, y)];
        to = [rotator transformPoint: NSMakePoint (maxX, y)];
        [NSBezierPath strokeLineFromPoint: from
                      toPoint: to];
    }
    for (y = topLeftSquare.origin.y - CELL_SIZE;
         y > minY;
         y -= CELL_SIZE) {
        from = [rotator transformPoint: NSMakePoint(minX, y)];
        to = [rotator transformPoint: NSMakePoint (maxX, y)];
        [NSBezierPath strokeLineFromPoint: from
                      toPoint: to];
    }

} // drawGrid


- (void) drawSeperatorInRect: (NSRect) rect
                   direction: (BWQueenStitch_Direction) direction
{
    // 012345678
    //  * * * *
    // 3-point segments, and start 3 into it so the line starts
    // with empty segment
    float dash[6] = { 5.0, 3.0, 5.0, 3.0, 5.0, 3.0 };

    NSBezierPath *path = [NSBezierPath bezierPath];
    [path setLineWidth: 1.5];
    [path setLineDash: dash  count: sizeof(dash) / sizeof(*dash)
          phase: 6.0];

    NSPoint bottom, top;
    if (direction == kDirection_NorthSouth) {
        // bottom-left upper-right
        bottom = NSMakePoint (NSMinX(rect), NSMinY(rect));
        top = NSMakePoint (NSMaxX(rect), NSMaxY(rect));
    } else {
        // top-left bottom-right
        bottom = NSMakePoint (NSMinX(rect), NSMaxY(rect));
        top = NSMakePoint (NSMaxX(rect), NSMinY(rect));
    }

    [path moveToPoint: top];
    [path lineToPoint: bottom];
    
    [[NSColor lightGrayColor] set];
    [path stroke];

} // drawSeperatorInRect

    
- (void) drawStitches
{
    NSEnumerator *enumerator = [stitches objectEnumerator];

    BWQueenStitch *stitch;
    while (stitch = [enumerator nextObject]) {
        BWGrid_Square square = { [stitch x], [stitch y] };
        NSRect rect = [self rectForSquare: square];
        NSColor *color1 = [stitch color1];
        NSColor *color2 = [stitch color2];

        if (color2 == nil) {
            [color1 set];
            NSRectFill (rect);

        } else {
            NSPoint p1, p2, p3, p4;

            // otherwise a partial stitch
            if ([stitch direction] == kDirection_NorthSouth) {
                // top-left to bottom-right to bottom-left, and 
                // top-left to bottom-right to top-right
                p1 = NSMakePoint (NSMinX(rect), NSMaxY(rect));
                p2 = NSMakePoint (NSMaxX(rect), NSMinY(rect));
                p3 = NSMakePoint (NSMinX(rect), NSMinY(rect));
                p4 = NSMakePoint (NSMaxX(rect), NSMaxY(rect));

            } else {
                // top-right to bottom-left to bottom right
                // top-right to bottom-left to top-left
                p1 = NSMakePoint (NSMaxX(rect), NSMaxY(rect));
                p2 = NSMakePoint (NSMinX(rect), NSMinY(rect));
                p3 = NSMakePoint (NSMaxX(rect), NSMinY(rect));
                p4 = NSMakePoint (NSMinX(rect), NSMaxY(rect));
            }
            NSBezierPath *path = [NSBezierPath bezierPath];

            [path moveToPoint: p1];
            [path lineToPoint: p2];
            [path lineToPoint: p3];
            [path closePath];
            [color1 set];
            [path fill];

            [path removeAllPoints];
            [path moveToPoint: p1];
            [path lineToPoint: p2];
            [path lineToPoint: p4];
            [path closePath];
            [color2 set];
            [path fill];
        }

        [self drawSeperatorInRect: rect
            direction: [stitch direction]];
    }

} // drawStitches


- (void) drawFrenchKnots
{
    NSEnumerator *enumerator = [frenchKnots objectEnumerator];

    BWFrenchKnot *knot;
    while (knot = [enumerator nextObject]) {
        BWGrid_Square square = { [knot x], [knot y] };

        NSRect rect = [self rectForIntersection: square];

        [[NSColor blackColor] set];
        NSBezierPath *path;
        path = [NSBezierPath bezierPathWithOvalInRect: rect];
        [path fill];
    }

} // drawFrenchKnots


- (void) drawSelection
{
    NSRect rect;
    rect = [self rectForSquare: selectedSquare];

    NSBezierPath *path;
    path = [NSBezierPath bezierPathWithRect: rect];
    [path setLineWidth: 2.0];

    [[NSColor blackColor] set];
    [path stroke];

} // drawSelection


// a "rotated" rectangle connecting the two anchors.  From the user's POV
// it's not rotated

- (NSRect) unrotatedArea
{
    NSRect anchorRect, oppositeRect;

    anchorRect = [self rectForSquare: areaAnchor];
    oppositeRect = [self rectForSquare: areaOpposite];

    NSPoint anchor, opposite;
    anchor = NSMakePoint (anchorRect.origin.x + anchorRect.size.width / 2.0,
                          anchorRect.origin.y + anchorRect.size.height / 2.0);
    opposite = NSMakePoint (oppositeRect.origin.x 
                            + oppositeRect.size.width / 2.0,
                            oppositeRect.origin.y 
                            + oppositeRect.size.height / 2.0);

    anchor = [rotator transformPoint: anchor];
    opposite = [rotator transformPoint: opposite];

    NSRect rect = rectFromPoints (anchor, opposite);

    return (rect);

} // unrotatedArea


- (NSBezierPath *) areaRectPath
{
    NSRect rect = [self unrotatedArea];

    NSPoint topLeft, topRight, bottomLeft, bottomRight;
    topLeft = NSMakePoint (NSMinX(rect), NSMinY(rect));
    topRight = NSMakePoint (NSMaxX(rect), NSMinY(rect));
    bottomLeft = NSMakePoint (NSMinX(rect), NSMaxY(rect));
    bottomRight = NSMakePoint (NSMaxX(rect), NSMaxY(rect));

    topLeft = [inverseRotator transformPoint: topLeft];
    topRight = [inverseRotator transformPoint: topRight];
    bottomLeft = [inverseRotator transformPoint: bottomLeft];
    bottomRight = [inverseRotator transformPoint: bottomRight];

    NSBezierPath *path;
    path = [NSBezierPath bezierPath];

    [path moveToPoint: topLeft];
    [path lineToPoint: topRight];
    [path lineToPoint: bottomRight];
    [path lineToPoint: bottomLeft];
    [path closePath];

    return (path);

} // areaRectPath


- (void) drawArea
{
    NSBezierPath *path = [self areaRectPath];

    NSColor *color;
    color = [NSColor colorWithDeviceWhite: 0.9  alpha: 0.5];

    [color set];
    [path fill];

    [[NSColor blackColor] set];
    [path stroke];

} // drawArea


- (void) drawRect: (NSRect) rect
{
    NSRect bounds = [self bounds];

    [backgroundColor set];
    NSRectFill (bounds);

    [NSGraphicsContext saveGraphicsState]; {
        [rotator concat];
        [self drawStitches];
        [self drawFrenchKnots];
    } [NSGraphicsContext restoreGraphicsState];

    // drawing the grid is *hideously* slow in the rotated world
    [[NSColor blackColor] set];
    [self drawGrid];

    [NSGraphicsContext saveGraphicsState]; {
        [rotator concat];
        if (isSelected) {
            [self drawSelection];
        }

        if (showArea) {
            [self drawArea];
        }

    } [NSGraphicsContext restoreGraphicsState];

    [[NSColor blackColor] set];
    NSFrameRect (bounds);

} // drawRect



- (NSPoint) transformedMouse: (NSEvent *) event
{
    NSPoint mouse;
    mouse = [self convertPoint: [event locationInWindow]
                  fromView: nil];
    mouse = [inverseRotator transformPoint: mouse];

    return (mouse);

} // transformedMouse


- (void) trackMouse: (NSEvent *) event
               mode: (BWDiamondCanvas_SelectMode) mode
{
    if (!shouldTrack) {
        return;
    }

    NSPoint mouse = [self transformedMouse: event];
    BWGrid_Square newSquare;

    if (trackMode == kTrackDiamonds) {
        [self mapPoint: mouse  toSquare: &newSquare];
        [delegate canvas: self  selectionAtX: newSquare.x  y: newSquare.y
                  mode: mode];

    } else if (trackMode == kTrackIntersections) {
        [self mapPoint: mouse  toSquareIntersection: &newSquare];
        [delegate canvas: self  selectionAtX: newSquare.x  y : newSquare.y
                  mode: mode];
    }

} // trackMouse


- (void) mouseDown: (NSEvent *) event
{
    [self trackMouse: event  mode: kMouseDown];

} // mouseDown



- (void) mouseUp: (NSEvent *) event
{
    [self trackMouse: event  mode: kMouseUp];

} // mouseUp


- (void) mouseDragged: (NSEvent *) event
{
    [self trackMouse: event  mode: kMouseDragged];
} // mouseDragged


- (void) scrollWheel: (NSEvent *) event
{
    int horizontalDelta = 0;
    int verticalDelta = 0;

    NSPoint point = { [event deltaX], [event deltaY] };
    point = [inverseRotator transformPoint: point];

    if (point.x > 0.0) {
        horizontalDelta = -1;
    } else if (point.x < 0.0) {
        horizontalDelta = 1;
    }

    if (point.y > 0.0) {
        verticalDelta = -1;
    } else if (point.y < 0.0) {
        verticalDelta = 1;
    }

    [delegate canvas: self
              scrolledX: horizontalDelta
              y: verticalDelta];

} // scrollWheel




- (BOOL) acceptsFirstResponder
{
    return (YES);
} // acceptsFirstResponder


- (BOOL) isFlipped
{
    return (YES);
} // isFlipped


- (void) setDelegate: (id) d
{
    delegate = d;
} // setDelegate


- (void) setStitches: (BWQueenStitchSet *) s
{
    [s retain];
    [stitches release];

    stitches = s;

} // setStitches


- (void) setFrenchKnots: (BWFrenchKnotSet *) k
{
    [k retain];
    [frenchKnots release];

    frenchKnots = k;

} // setFrenchKnots



- (void) setShouldTrack: (BOOL) st;
{
    shouldTrack = st;
} // setShouldTrack


- (void) selectX: (int) x  y: (int) y
{
    isSelected = YES;

    if (x != selectedSquare.x || y != selectedSquare.y) {
        [self setNeedsDisplayInSquare: selectedSquare];
        
        selectedSquare.x = x;
        selectedSquare.y = y;
        
        [self setNeedsDisplayInSquare: selectedSquare];
    }

} // selectX


- (void) unselectAll
{
    isSelected = NO;
    [self setNeedsDisplay: YES];

} // unselectAll


- (void) setOffsetX: (float) x  y: (float) y
{
    rotator = [[NSAffineTransform alloc] init];
    [rotator translateXBy: x
             yBy: y];
    [rotator rotateByDegrees: 45];
    inverseRotator = [rotator copy];
    [inverseRotator invert];

} // setOffsetX


- (void) setShowArea: (BOOL) sa
{
    showArea = sa;
    [self setNeedsDisplay: YES];

} // setShowArea


- (void) setAreaAnchor: (BWGrid_Square) start
{
    areaAnchor = start;

    if (showArea) {
        [self setNeedsDisplay: YES];
    }

} // setAreaAnchor


- (void) setAreaOpposite: (BWGrid_Square) opposite
{
    areaOpposite = opposite;

    if (showArea) {
        [self setNeedsDisplay: YES];
    }

} // setAreaOpposite


- (BWGrid_Square) areaAnchor
{
    return (areaAnchor);

} // areaAnchor

- (BWGrid_Square) areaOpposite
{
    return (areaOpposite);
} // return areaOpposite


- (void) invalRotatedRect: (NSRect) rect
{
    NSPoint topLeft, topRight, bottomLeft, bottomRight;
    topLeft = NSMakePoint (NSMinX(rect), NSMaxY(rect));
    topRight = NSMakePoint (NSMaxX(rect), NSMaxY(rect));
    bottomLeft = NSMakePoint (NSMinX(rect), NSMinY(rect));
    bottomRight = NSMakePoint (NSMaxX(rect), NSMinY(rect));

    topLeft = [rotator transformPoint: topLeft];
    topRight = [rotator transformPoint: topRight];
    bottomLeft = [rotator transformPoint: bottomLeft];
    bottomRight = [rotator transformPoint: bottomRight];

    rect = rectFrom4Points (topLeft, topRight, bottomLeft, bottomRight);

    [self setNeedsDisplayInRect: rect];

} // invalRotatedRect

- (void) setNeedsDisplayInSquare: (BWGrid_Square) square
{
    NSRect rect = [self rectForSquare: square];
    [self invalRotatedRect: rect];

} //setNeedsDisplayInSquare


- (void) setNeedsDisplayAtIntersection: (BWGrid_Square) square
{
    NSRect rect = [self rectForIntersection: square];
    [self invalRotatedRect: rect];

} // setNeedsDisplayAtIntersection



- (void) setBackgroundColor: (NSColor *) color
{
    [backgroundColor release];
    backgroundColor = [color copy];

    [self setNeedsDisplay: YES];

} // setBackgroundColor


- (NSColor *) backgroundColor
{
    return (backgroundColor);

} // backgroundColorp


- (void) setZoom: (int) zoomFactor
{
    if (zoom != zoomFactor) {
        zoom = zoomFactor;

        // scale back to unit == 1.0 x 1.0
        NSSize unit = NSMakeSize (1.0, 1.0);
        unit = [self convertSize: unit  toView: nil];
        unit.width = 1.0 / unit.width;
        unit.height = 1.0 / unit.height;
        [self scaleUnitSquareToSize: unit];

        // now get the zoom the user wants
        NSSize zoomSize = NSMakeSize (zoomFactor / 100.0,
                                      zoomFactor / 100.0);
        [self scaleUnitSquareToSize: zoomSize];
        [self setNeedsDisplay: YES];
        // if in a scroller, adjust frame
    }

} // setZoom


- (void) setTrackMode: (BWDiamondCanvas_TrackStyle) mode
{
    trackMode = mode;
} // setTrackMode

@end // BWDiamondCanvas

